/**
 * (C) Copyright 2004-2007 Mort Bay Consulting Pty. Ltd.
 * (C) Copyright 2007 Polar Rose AB & Stefan Arentz
 *
 *   Parts of this code was taken from the Jetty project, which can be
 *   found at http://www.mortbay.org/jetty
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.sourceforge.stripes.authentication;

import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.config.BootstrapPropertyResolver;
import net.sourceforge.stripes.config.ConfigurableComponent;
import net.sourceforge.stripes.config.Configuration;
import net.sourceforge.stripes.controller.ExecutionContext;
import net.sourceforge.stripes.controller.Interceptor;
import net.sourceforge.stripes.controller.Intercepts;
import net.sourceforge.stripes.controller.LifecycleStage;
import net.sourceforge.stripes.util.ReflectUtil;
import net.sourceforge.stripes.integration.spring.SpringHelper;

import javax.servlet.http.HttpServletRequest;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Annotation used to mark ActionBeans as a whole or specific Event Handlers as requiring authentication. 
 *
 * @author Stefan Arentz
 */

@Intercepts(LifecycleStage.HandlerResolution)
public class AuthenticationInterceptor implements ConfigurableComponent, Interceptor
{
    /**
     * Name of the servlet filter's init-param that is used to configure the authentication service class name.
     */

    private final static String AUTHENTICATIONINTERCEPTOR_AUTHENTICATIONSERVICECLASS
        = "AuthenticationInterceptor.AuthenticationServiceClass";

    /**
     * The authentication service.
     */

    private AuthenticationService authenticationService;

    /**
     * The configuration.
     */

    private Configuration configuration;

    /**
     * Whether we are initialized or not.
     */

    private boolean initialized = false;

    /**
     *
     * @see net.sourceforge.stripes.config.ConfigurableComponent#init(net.sourceforge.stripes.config.Configuration)
     */

    public void init(Configuration configuration) throws Exception
    {
        this.configuration = configuration;
    }

    /**
     * @see net.sourceforge.stripes.controller.Interceptor#intercept(net.sourceforge.stripes.controller.ExecutionContext)
     */

    public Resolution intercept(ExecutionContext executionContext) throws Exception
    {
        // Initialize the authentication interceptor by finding and instantiating the authentication service and then
        // injecting Spring beans.

        if (!initialized)
        {
           // Find the class name of the authentication service

           BootstrapPropertyResolver bootstrapPropertyResolver = configuration.getBootstrapPropertyResolver();

           String authenticationServiceClassName = bootstrapPropertyResolver.getFilterConfig()
               .getInitParameter(AUTHENTICATIONINTERCEPTOR_AUTHENTICATIONSERVICECLASS);
           if (authenticationServiceClassName == null) {
               throw new Exception("AuthenticationInterceptor - Required configuration option '"
                   + AUTHENTICATIONINTERCEPTOR_AUTHENTICATIONSERVICECLASS + "' is missing.");
           }

           // Instantiate the authentication service

           Class authenticationServiceClass = ReflectUtil.findClass(authenticationServiceClassName);
           authenticationService = (AuthenticationService) authenticationServiceClass.newInstance();

           // Inject the authentication service with spring beans

           SpringHelper.injectBeans(authenticationService, configuration.getServletContext());

           initialized = true;
        }

        Resolution resolution = executionContext.proceed();

        ActionBean actionBean = executionContext.getActionBean();
        Class<? extends ActionBean> actionBeanClass = actionBean.getClass();

        // Determine if the current ActionBean requires authentication. If not found then determine if the current
        // HandlerMethod requires authentication.

        RequiresAuthentication requiresAuthentication = getAnnotationFromClass(actionBeanClass);
        if (requiresAuthentication == null) {
            Method handler = executionContext.getHandler();
            requiresAuthentication = handler.getAnnotation(RequiresAuthentication.class);
        }

        //

        if (requiresAuthentication != null)
        {
            AuthenticatedUserInfo authenticatedUserInfo = null;

            // Check if there is an Authorization header present

            HttpServletRequest request = executionContext.getActionBeanContext().getRequest();

            String authentication = request.getHeader("Authorization");
            if (authentication != null) {
                AbstractAuthenticationChecker checker = null;
                switch (requiresAuthentication.type()) {
                    case BASIC:
                        checker = new BasicAuthenticationChecker();
                        break;
                    case DIGEST:
                        checker = new DigestAuthenticationChecker();
                        break;
                }
                if (checker != null) {
                    authenticatedUserInfo = checker.check(authenticationService, request, authentication);
                }
            }

            // If we are not authenticated then send back a 401

            if (authenticatedUserInfo == null) {
                switch (requiresAuthentication.type()) {
                    case BASIC:
                        resolution = new BasicAuthenticationResolution(requiresAuthentication.realm());
                        break;
                    case DIGEST:
                        resolution = new DigestAuthenticationResolution(requiresAuthentication.realm());
                        break;
                }
            }

            // We are ok, so check if the actionbean has a field that is annotated with @AuthenticatedUser. This
            // field needs to have a correctly named setter method.

            else {
                Method method = getAuthenticatedUserSetterMethod(actionBeanClass);
                if (method != null) {
                    method.invoke(actionBean, authenticatedUserInfo.getGetUserInfo());
                }
            }
        }

        return resolution;
    }

    //

//    private final ConcurrentMap<Class,RequiresAuthentication> cachedAuthenticationByClass
//        = new ConcurrentHashMap<Class, RequiresAuthentication>();

    private RequiresAuthentication getAnnotationFromClass(Class<? extends ActionBean> actionBeanClass)
    {
        RequiresAuthentication requiresAuthentication = null;

        Class<?> clazz = actionBeanClass;
        while (clazz != null && ActionBean.class.isAssignableFrom(clazz)) {
            requiresAuthentication = clazz.getAnnotation(RequiresAuthentication.class);
            if (requiresAuthentication != null) {
                break;
            }
            clazz = clazz.getSuperclass();
        }

        return requiresAuthentication;
    }

    //

    private final ConcurrentMap<Class,Method> cachedSetterMethods = new ConcurrentHashMap<Class, Method>();

    private Method getAuthenticatedUserSetterMethod(Class<? extends ActionBean> clazz)
    {
        Method method = null;

        if (cachedSetterMethods.containsKey(clazz)) {
            method = cachedSetterMethods.get(clazz);
        } else {
            for (Field field : ReflectUtil.getFields(clazz)) {
                if (field.isAnnotationPresent(AuthenticatedUser.class)) {
                    PropertyDescriptor propertyDescriptor = ReflectUtil.getPropertyDescriptor(clazz, field.getName());
                    method = propertyDescriptor.getWriteMethod();
                    cachedSetterMethods.putIfAbsent(clazz, method);
                    break;
                }
            }
        }

        return method;
    }
}
